import logging

from django.db import models
from django.utils import timezone

from bitfield import BitField
from sentry.db.models import (
    BoundedPositiveIntegerField,
    FlexibleForeignKey,
    Model,
    control_silo_only_model,
    sane_repr,
)
from sentry.db.models.fields.jsonfield import JSONField
from sentry.models.organizationmember import OrganizationMember
from sentry.utils.http import absolute_uri

logger = logging.getLogger("sentry.authprovider")

SCIM_INTERNAL_INTEGRATION_OVERVIEW = (
    "This internal integration was auto-generated during the installation process of your SCIM "
    "integration. It is needed to provide the token used provision members and teams. If this integration is "
    "deleted, your SCIM integration will stop working!"
)


@control_silo_only_model
class AuthProviderDefaultTeams(Model):
    __include_in_export__ = False

    authprovider = FlexibleForeignKey("sentry.AuthProvider")
    team = FlexibleForeignKey("sentry.Team")

    class Meta:
        app_label = "sentry"
        db_table = "sentry_authprovider_default_teams"
        unique_together = (("authprovider", "team"),)


@control_silo_only_model
class AuthProvider(Model):
    __include_in_export__ = True

    organization = FlexibleForeignKey("sentry.Organization", unique=True)
    provider = models.CharField(max_length=128)
    config = JSONField()

    date_added = models.DateTimeField(default=timezone.now)
    sync_time = BoundedPositiveIntegerField(null=True)
    last_sync = models.DateTimeField(null=True)

    default_role = BoundedPositiveIntegerField(default=50)
    default_global_access = models.BooleanField(default=True)
    # TODO(dcramer): ManyToMany has the same issue as ForeignKey and we need
    # to either write our own which works w/ BigAuto or switch this to use
    # through.
    default_teams = models.ManyToManyField(
        "sentry.Team", blank=True, through=AuthProviderDefaultTeams
    )

    flags = BitField(
        flags=(
            ("allow_unlinked", "Grant access to members who have not linked SSO accounts."),
            ("scim_enabled", "Enable SCIM for member and team provisioning and syncing"),
        ),
        default=0,
    )

    class Meta:
        app_label = "sentry"
        db_table = "sentry_authprovider"

    __repr__ = sane_repr("organization_id", "provider")

    def __str__(self):
        return self.provider

    def get_provider(self):
        from sentry.auth import manager

        return manager.get(self.provider, **self.config)

    @property
    def provider_name(self) -> str:
        return self.get_provider().name

    def get_scim_token(self):
        from sentry.models import SentryAppInstallationToken

        if self.flags.scim_enabled:
            return SentryAppInstallationToken.objects.get_token(
                self.organization, f"{self.provider}_scim"
            )
        else:
            logger.warning(
                "SCIM disabled but tried to access token",
                extra={"organization_id": self.organization.id},
            )
            return None

    def get_scim_url(self):
        if self.flags.scim_enabled:
            # the SCIM protocol doesn't use trailing slashes in URLs
            return absolute_uri(f"api/0/organizations/{self.organization.slug}/scim/v2")

        else:
            return None

    def enable_scim(self, user):
        from sentry.mediators.sentry_apps import InternalCreator
        from sentry.models import SentryAppInstallation, SentryAppInstallationForProvider

        if (
            not self.get_provider().can_use_scim(self.organization, user)
            or self.flags.scim_enabled is True
        ):
            logger.warning(
                "SCIM already enabled",
                extra={"organization_id": self.organization.id},
            )
            return

        # check if we have a scim app already

        if SentryAppInstallationForProvider.objects.filter(
            organization=self.organization, provider="okta_scim"
        ).exists():
            logger.warning(
                "SCIM installation already exists",
                extra={"organization_id": self.organization.id},
            )
            return

        data = {
            "name": "SCIM Internal Integration",
            "author": "Auto-generated by Sentry",
            "organization": self.organization,
            "overview": SCIM_INTERNAL_INTEGRATION_OVERVIEW,
            "user": user,
            "scopes": [
                "member:read",
                "member:write",
                "member:admin",
                "team:write",
                "team:admin",
            ],
        }
        # create the internal integration and link it to the join table
        sentry_app = InternalCreator.run(**data)
        sentry_app_installation = SentryAppInstallation.objects.get(sentry_app=sentry_app)
        SentryAppInstallationForProvider.objects.create(
            sentry_app_installation=sentry_app_installation,
            organization=self.organization,
            provider=f"{self.provider}_scim",
        )
        self.flags.scim_enabled = True

    def _reset_idp_flags(self):
        OrganizationMember.objects.filter(
            organization=self.organization,
            flags=models.F("flags").bitor(OrganizationMember.flags["idp:provisioned"]),
        ).update(
            flags=models.F("flags")
            .bitand(~OrganizationMember.flags["idp:provisioned"])
            .bitand(~OrganizationMember.flags["idp:role-restricted"])
        )

    def disable_scim(self, user):
        from sentry import deletions
        from sentry.models import SentryAppInstallationForProvider

        if self.flags.scim_enabled:
            install = SentryAppInstallationForProvider.objects.get(
                organization=self.organization, provider=f"{self.provider}_scim"
            )
            # Only one SCIM installation allowed per organization. So we can reset the idp flags for the orgs
            # We run this update before the app is uninstalled to avoid ending up in a situation where there are
            # members locked out because we failed to drop the IDP flag
            self._reset_idp_flags()
            sentry_app = install.sentry_app_installation.sentry_app
            assert (
                sentry_app.is_internal
            ), "scim sentry apps should always be internal, thus deleting them without triggering InstallationNotifier is correct."
            deletions.exec_sync(sentry_app)
            self.flags.scim_enabled = False

    def get_audit_log_data(self):
        return {"provider": self.provider, "config": self.config}
